// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.search

import android.content.Context
import android.location.Location
import android.os.Parcelable
import android.util.Log
import com.google.openlocationcode.OpenLocationCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.parcelize.Parcelize
import xyz.apiote.bimba.czwek.data.exceptions.BimbaException
import xyz.apiote.bimba.czwek.data.traffic.PointOfInterest
import xyz.apiote.bimba.czwek.data.traffic.TrafficRepository
import xyz.apiote.bimba.czwek.repo.Position

@Parcelize
class Query(var raw: String, var mode: Mode, var position: Position?) : Parcelable {
	@Parcelize
	enum class Mode : Parcelable {
		LOCATION, POSITION, NAME, LOCATION_PLUS_CODE, UNKNOWN
	}

	constructor(raw: String) : this(raw, Mode.UNKNOWN, null)

	constructor(raw: String, mode: Mode) : this(raw, mode, null)

	constructor(mode: Mode) : this("", mode, null) {
		if (mode != Mode.LOCATION) {
			throw Exception("Cannot initialise Query from bare Mode other than LOCATION")
		}
	}

	constructor(position: Position) : this(position.toString(), Mode.POSITION, position)

	private constructor(mode: Mode, raw: String, position: Position?) : this(raw, mode, position)

	fun willNeedGeocoding(): Boolean {
		return !OpenLocationCode.isValidCode(raw) && OpenLocationCode.isValidCode(
			raw.trim().split(" ").first().trim(',').trim()
		)
	}

	fun parse(context: Context) {
		if (mode != Mode.UNKNOWN) {
			return
		}
		if (OpenLocationCode.isValidCode(raw)) {
			val olc = OpenLocationCode(raw)
			if (!olc.isFull) {
				mode = Mode.LOCATION_PLUS_CODE
			} else {
				val area = olc.decode()
				mode = Mode.POSITION
				position = Position(area.centerLatitude, area.centerLongitude)
			}
		} else if (willNeedGeocoding()) {
			geocode(context)
		} else if (seemsCoordinatesDegrees(raw)) {
			val coordinates = raw.split(", ", ",", " ")
			try {
				position = Position(Location.convert(coordinates[0]), Location.convert(coordinates[1]))
				mode = Mode.POSITION
			} catch (e: Exception) {
				Log.i("Query", "while parsing degrees: $e")
				mode = Mode.NAME
			}
		} else if (seemsCoordinatesDegreesMinutesSeconds(raw)) {
			val coordinates =
				raw.replace(Regex("° ?"), ":").replace(Regex("' ?"), ":").replace(Regex("""" ?"""), "")
					.split(" ").map { it.replace(",", "") }.toMutableList()
			try {
				val northSouth = if (coordinates[0].last() == 'N') 1 else -1
				val eastWest = if (coordinates[1].last() == 'E') 1 else -1
				coordinates[0] = coordinates[0].replace(Regex("[NS]"), "")
				coordinates[1] = coordinates[1].replace(Regex("[EW]"), "")
				position =
					Position(Location.convert(coordinates[0]) * northSouth, Location.convert(coordinates[1]) * eastWest)
				mode = Mode.POSITION
			} catch (e: Exception) {
				Log.i("Query", "while parsing deg min sec: $e")
				mode = Mode.NAME
			}
		} else {
			mode = Mode.NAME
		}
	}

	private fun seemsCoordinatesDegrees(s: String): Boolean {
		return Regex("""[+-]?[0-9]+(\.[0-9]+)?(,| |, )[+-]?[0-9]+(\.[0-9]+)?""").matches(s)
	}

	private fun seemsCoordinatesDegreesMinutesSeconds(s: String): Boolean {
		return Regex("""[0-9]+° ?[0-9]+' ?[0-9]+(\.[0-9]+)?" ?[NS] [0-9]+° ?[0-9]+' ?[0-9]+(\.[0-9]+)?" ?[EW]""").matches(
			s
		)
	}

	private fun geocode(context: Context) {
		val split = raw.trim().split(" ")
		val code = split.first().trim(',').trim()
		val freePart = split.drop(1).joinToString(" ")

		val repository = TrafficRepository()
		val flowResult = runBlocking(Dispatchers.IO) {
			repository.queryPlaces(freePart, context).firstOrNull()
		}

		if (flowResult == null) {
			return
		}

		if (flowResult is BimbaException) {
			throw flowResult
		}

		val poi = flowResult as PointOfInterest

		val area = OpenLocationCode(code).recover(
			poi.poiPosition.positionLatitude,
			poi.poiPosition.positionLongitude
		).decode()
		position = Position(area.centerLatitude, area.centerLongitude)
		mode = Mode.POSITION
	}

	override fun toString(): String {
		return when (mode) {
			Mode.UNKNOWN -> raw
			Mode.LOCATION -> "here"
			Mode.POSITION -> "%.2f, %.2f".format(
				position!!.positionLatitude,
				position!!.positionLongitude
			)  // TODO settings for position format
			Mode.NAME -> raw
			Mode.LOCATION_PLUS_CODE -> raw
		}
	}
}